/*
 * <h3>Title:wheel-parent系统</h3>
 * <p>Copyright (c) 2019, wnfbx@163.com. All rights reserved.</p>
 * @author wnfbx@163.com
 */
package com.wheel.autoconfigure.dynamicdatasource.builder;

import com.wheel.autoconfigure.dynamicdatasource.DynamicDataSourceProperties;
import com.wheel.autoconfigure.dynamicdatasource.configbinder.DynamicConfigurationPropertiesBinder;
import com.wheel.autoconfigure.dynamicdatasource.exception.CreateDataSourceException;
import com.wheel.autoconfigure.dynamicdatasource.loadbalancer.DataSourceLoadBalancer;
import com.wheel.autoconfigure.dynamicdatasource.processor.DynamicDataSourceDetermineProcessor;
import com.wheel.core.utils.Log;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.ClassUtils;

import javax.sql.DataSource;
import javax.sql.XADataSource;
import java.util.*;

/**
 * <dt>Description:DynamicDataSourceBuilder</dt>
 * <dd>创建数据源</dd>
 *
 * @author wnfbx@163.com
 * @version 1.0 <i>2019/12/17 9:11</i>
 * @since jdk1.7+
 */
public class DynamicDataSourceBuilder {

    /**
     * DRUID数据源类
     */
    private static final String DRUID_DATASOURCE = "com.alibaba.druid.pool.DruidDataSource";
    private static final String DRUID_XA_DATASOURCE = "com.alibaba.druid.pool.xa.DruidXADataSource";
    /**
     * HikariCp数据源
     */
    private static final String HIKARI_DATASOURCE = "com.zaxxer.hikari.HikariDataSource";
    private static final String PROPERTY_KEY_TYPE = "type";
    private static final String PROPERTY_KEY_JNDI_NAME = "jndi-name";
    private static final String PROPERTY_KEY_XA_DATA_SOURCE_CLASS_NAME = "xa-data-source-class-name";

    /**
     * 系统配置的所有数据源
     */
    private Map<String, DataSource> dataSourceMap;
    /**
     * 主数据源分组
     */
    private List<String> masters;
    /**
     * 从数据源分组
     */
    private List<String> slaves;
    /**
     * 是否需要分布式事务
     */
    private boolean isJTA = false;

    private DynamicDataSourceProperties dynamicDataSourceProperties;
    private DynamicDataSourceDetermineProcessor determineProcessor;

    /**
     * <dt>Description:DynamicDataSourceBuilder</dt>
     * <dd>对所有数据源配置进行构建</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2019/12/18 15:38</i></dd>
     *
     * @param dynamicDataSourceProperties 动态数据源配置
     * @since jdk1.7+
     */
    public DynamicDataSourceBuilder(DynamicDataSourceProperties dynamicDataSourceProperties) {
        this.dynamicDataSourceProperties = dynamicDataSourceProperties;
    }

    /**
     * <dt>Description:build</dt>
     * <dd>主要方法</dd>
     * <dd>author: wnfbx</dd>
     * <dd>version:1.0 <i>2021/8/12 17:26</i></dd>
     *
     * @return com.wheel.autoconfigure.dynamicdatasource.builder.DynamicDataSourceBuilder
     * @since jdk1.7+
     */
    public DynamicDataSourceBuilder build() {
        Map<String, Properties> dataSourcePropertiesMap = dynamicDataSourceProperties.getConfig();
        int size = dataSourcePropertiesMap.size();
        dataSourceMap = new HashMap<>(size);
        masters = new ArrayList<>(size);
        slaves = new ArrayList<>(size);
        // 生成主从库索引
        String masterPerfix = dynamicDataSourceProperties.getMasterPerfix().trim();
        for (String key : dataSourcePropertiesMap.keySet()) {
            if (checkMaster(key, masterPerfix)) {
                masters.add(key);
            } else {
                slaves.add(key);
            }
        }
        //如果有多套主库，需要做JTA事务
        if (masters.size() > 1) {
            isJTA = true;
        } else if (masters.size() == 0) {
            throw new CreateDataSourceException("没找到前缀为[" + masterPerfix + "]的datasource配置");
        }
        //没配置从库，主从同库
        if (slaves.size() == 0) {
            slaves = masters;
        }

        for (Map.Entry<String, Properties> item : dataSourcePropertiesMap.entrySet()) {

            String key = item.getKey();
            Properties properties = item.getValue();

            DataSource dataSource;

            //生成jndi数据源 TODO 无场景未验证
            String jndiName = getJndiName(properties);
            if (jndiName != null && jndiName.trim().length() > 0) {
                if (isJTA && checkMaster(key, masterPerfix)) {
                    dataSource = buildXaJNDIDataSource(key, jndiName);
                } else {
                    dataSource = buildJNDIDataSource(jndiName);
                }
            }
            // 如果启用jta 只处理写库
            if (isJTA && checkMaster(key, masterPerfix)) {
                dataSource = buildXaDataSource(key, dynamicDataSourceProperties, properties);
            } else {
                dataSource = buildDataSource(dynamicDataSourceProperties, properties);
            }
            dataSourceMap.put(key, dataSource);
        }

        buildDetermineProcessor();
        return this;
    }

    /**
     * <dt>Description:checkMaster</dt>
     * <dd>判断是不是主数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/9 16:26</i></dd>
     *
     * @param key          数据源key
     * @param masterPerfix 主数据源前缀
     * @return boolean
     * @since jdk1.7+
     */
    public boolean checkMaster(String key, String masterPerfix) {
        key = key.trim();
        return key.length() > 0 && key.startsWith(masterPerfix);
    }


    private DataSource buildXaJNDIDataSource(String key, String jndiName) {
        XADataSource dataSource = (XADataSource) new JndiDataSourceLookup().getDataSource(jndiName);
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setUniqueResourceName(key);
        xaDataSource.setXaDataSource(dataSource);
        return xaDataSource;
    }

    private String getXaDataSourceClassName(Properties properties) {
        return getSpringConfig(properties, PROPERTY_KEY_XA_DATA_SOURCE_CLASS_NAME);
    }

    private String getJndiName(Properties properties) {
        return getSpringConfig(properties, PROPERTY_KEY_JNDI_NAME);
    }

    /**
     * <dt>Description:getSpringConfig</dt>
     * <dd>按照spring规范获取配置</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/7 18:42</i></dd>
     *
     * @param properties 配置
     * @param key        属性名
     * @return java.lang.String
     * @since jdk1.7+
     */
    private String getSpringConfig(Properties properties, String key) {
        ConfigurationProperty configurationProperty = new MapConfigurationPropertySource(properties).withAliases(DynamicConfigurationPropertiesBinder.aliases).getConfigurationProperty(ConfigurationPropertyName.of(key));
        return configurationProperty == null ? null : (String) configurationProperty.getValue();
    }

    /**
     * <dt>Description:createDataSource</dt>
     * <dd>创建数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2019/12/18 14:27</i></dd>
     *
     * @param dynamicDataSourceProperties 通用配置
     * @param properties                  数据源配置属性
     * @return javax.sql.DataSource
     * @since jdk1.7+
     */
    private DataSource buildDataSource(DynamicDataSourceProperties dynamicDataSourceProperties, Properties properties) {
        String type = properties.getProperty(PROPERTY_KEY_TYPE);
        // 如果没配置数据源类型，则系统首推Druid
        Class<? extends DataSource> c = getRecommendDataSourceType(type);
        type = c.getName();
        Properties marge;
        if (DRUID_DATASOURCE.equals(type)) {
            marge = margeGlobal(dynamicDataSourceProperties.getGlobalDruidConfig(), properties);
        } else if (HIKARI_DATASOURCE.equals(type)) {
            marge = margeGlobal(dynamicDataSourceProperties.getGlobalHikariConfig(), properties);
        } else {
            marge = properties;
        }

        return DynamicConfigurationPropertiesBinder.create().bind(c, marge);
    }

    /**
     * <dt>Description:buildXaDataSource</dt>
     * <dd>创建XA数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/7 18:43</i></dd>
     *
     * @param key                         数据源名称
     * @param dynamicDataSourceProperties 通用配置
     * @param properties                  数据源配置属性
     * @return javax.sql.DataSource
     * @since jdk1.7+
     */
    private DataSource buildXaDataSource(String key, DynamicDataSourceProperties dynamicDataSourceProperties, Properties properties) {
        Properties marge = margeGlobal(dynamicDataSourceProperties.getGlobalDruidConfig(), properties);
        String xaDataSourceClassName = getXaDataSourceClassName(marge);
        Class<? extends DataSource> recommendDataSourceType = getRecommendXaDataSourceType(xaDataSourceClassName);
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSourceClassName(recommendDataSourceType.getName());
        xaDataSource.setUniqueResourceName(key);
        return DynamicConfigurationPropertiesBinder.create().bind(xaDataSource, marge);
    }

    /**
     * <dt>Description:margeGlobal</dt>
     * <dd>合并配置</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/5 18:48</i></dd>
     *
     * @param globalConfig 通用配置
     * @param properties   自定义配置
     * @return java.util.Properties
     * @since jdk1.7+
     */
    private Properties margeGlobal(Properties globalConfig, Properties properties) {
        Properties props = new Properties();
        if (globalConfig != null) {
            props.putAll(globalConfig);
        }
        props.putAll(properties);
        return props;
    }

    /**
     * <dt>Description:getRecommendDataSourceType</dt>
     * <dd>获取本模块推荐数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/4 14:41</i></dd>
     *
     * @param type 数据源类名
     * @return java.lang.Class<? extends javax.sql.DataSource>
     * @since jdk1.7+
     */
    @SuppressWarnings("unchecked")
    private Class<? extends DataSource> getRecommendDataSourceType(String type) {
        if (type != null) {
            try {
                return (Class<? extends DataSource>) ClassUtils.forName(type, null);
            } catch (ClassNotFoundException e) {
                throw new CreateDataSourceException(Log.formatMessage("配置数据源 class of type {} 不存在", type));
            }
        }
        try {
            // 尝试返回druid
            return (Class<? extends DataSource>) Class.forName(DRUID_DATASOURCE);
        } catch (ClassNotFoundException e) {
            return HikariDataSource.class;
        }
    }

    /**
     * <dt>Description:getRecommendXaDataSourceType</dt>
     * <dd>获取本模块推荐XA    数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2020/1/7 18:46</i></dd>
     *
     * @param type 数据源类名
     * @return java.lang.Class<? extends javax.sql.DataSource>
     * @since jdk1.7+
     */
    @SuppressWarnings("unchecked")
    private Class<? extends DataSource> getRecommendXaDataSourceType(String type) {
        if (type != null) {
            try {
                return (Class<? extends DataSource>) ClassUtils.forName(type, null);
            } catch (ClassNotFoundException e) {
                throw new CreateDataSourceException(Log.formatMessage("配置数据源 xa-data-source-class-name of type {} 不存在", type));
            }
        }
        try {
            // 尝试返回druid
            return (Class<? extends DataSource>) Class.forName(DRUID_XA_DATASOURCE);
        } catch (ClassNotFoundException e) {
            throw new CreateDataSourceException(Log.formatMessage("尝试 xa-data-source-class-name of type {} 不存在，多个主数据源，必须按照atomikos 的JTA配置，目前不支持其他JTA实现", DRUID_XA_DATASOURCE));
        }
    }

    /**
     * <dt>Description:createJNDIDataSource</dt>
     * <dd>查找jndi数据源</dd>
     * <dd>author: wnfbx@163.com</dd>
     * <dd>version:1.0 <i>2019/12/18 13:17</i></dd>
     *
     * @param dataSourceName 数据源配置名称
     * @return javax.sql.DataSource
     * @see org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
     * @since jdk1.7+
     */
    private DataSource buildJNDIDataSource(String dataSourceName) {
        return new JndiDataSourceLookup().getDataSource(dataSourceName);
    }

    public DynamicDataSourceDetermineProcessor getDetermineProcessor() {
        return determineProcessor;
    }

    private void buildDetermineProcessor() {
        DynamicDataSourceDetermineProcessor dynamicDataSourceDetermineProcessor;
        try {
            dynamicDataSourceDetermineProcessor = dynamicDataSourceProperties.getDetermineProcessor().newInstance();
            dynamicDataSourceDetermineProcessor.setDataSourceKeys(masters, slaves);
            dynamicDataSourceDetermineProcessor.setJTA(isJTA);

            this.determineProcessor = dynamicDataSourceDetermineProcessor;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new CreateDataSourceException("dynamicDataSourceDetermineProcessor 实例化出错 ", e);
        }
        try {
            DataSourceLoadBalancer masterBalancer = dynamicDataSourceProperties.getMasterBalancer().newInstance();
            DataSourceLoadBalancer slaveBalancer = dynamicDataSourceProperties.getSlaveBalancer().newInstance();
            dynamicDataSourceDetermineProcessor.setBalancer(masterBalancer, slaveBalancer);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new CreateDataSourceException("loadBalancer 实例化出错 ", e);
        }
    }


    public Map<String, DataSource> getDataSourceMap() {
        return dataSourceMap;
    }
}
